/*

* Copyright (c) 2007, National University of Singapore (NUS)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without 
* modification, are permitted provided that the following conditions 
* are met:
*
*   * Redistributions of source code must retain the above copyright notice, 
*     this list of conditions,the authors and the following disclaimer.
*   * Redistributions in binary form must reproduce the above copyright notice,
*     this list of conditions,the authors and the following disclaimer in
*     the documentation and/or other materials provided with the distribution.
*   * Neither the name of the university nor the names of its 
*     contributors may be used to endorse or promote products derived from 
*     this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
* POSSIBILITY OF SUCH DAMAGE.
* 
* Author: Wenwei Xue (dcsxw@nus.edu.sg)
*
*/
package psg.queryprocessor;

import java.util.*;
import psg.servicemanager.*;

/**
 * This class implements a very simple query processor at a PSG.
 */
public class QueryProcessor {
	
	public ContextDataServices cds;
	
	public QueryProcessor (ContextDataServices cds) {
		
		this.cds = cds;
				
	}	
    
	/**
	 * This function parses an SQL-based query and returns an evaluation plan.
	 * @param query the SQL query string
	 * @return the evaluation plan for the query; NULL if the input query string is in wrong syntax
	 */
	public QueryPlan parseQuery (String query) {
		
		QueryPlan q = new QueryPlan();
		
		q.cds = cds;
		
		q.queryString = query;
		
		String originQuery = query;
		query = query.toLowerCase().trim();
		
		int start, end, mark;
		
		// check the SELECT/SUBSCRIBE keyword
		start = query.indexOf("select ");
		if (start != -1)
		{
			q.type = QueryPlan.DATA_ACQUISITION;
			
			start += "select ".length();
		}
		else
		{
			start = query.indexOf("subscribe ");
			
			if (start != -1)
				q.type = QueryPlan.EVENT_SUBSCRIPTION;
			else
			{
				q.errStr = "Invalid query string: '" + query + "' \r\n";
				q.errStr += "SELECT or SUBSCRIBE clause is missing";
				return q;
			}
			
			start += "subscribe ".length();				
		}
		
		
		// check the FROM keyword, this keyworkd is optional
		boolean noFROM = false;
		boolean noWHERE = false;
		end = query.indexOf(" from ", start);
		if (end != -1)
		{
			mark = end + " from ".length();
		}
		else
		{
			noFROM = true;
			
			end = query.indexOf(" where ",start);
			
			if (end != -1)
				mark = end + " where ".length();
			else
			{
				noWHERE = true;
				end = mark = query.length();				
			}
		}
		
		// parse the SELECT clause: attribute list
		String str;
		Vector v;
		//str = query.substring(start, end).trim();
		str = originQuery.substring(start, end).trim();
		
		start = 0;
		end = str.indexOf(",", start);
		if (end >= mark) end=-1;
		v = new Vector(5,5);
		while (end!=-1)
		{
			v.addElement((String)str.substring(start,end).trim());
			start = end + 1;
			end = str.indexOf(",", start);
			if (end >= mark) end=-1;
		}
		end = str.length();
		v.addElement((String)str.substring(start,end).trim());
		
		q.attrs = new String [v.size()];
		int i,j;
		for (i=0;i<v.size();i++)
		{
			q.attrs[i] = (String)v.elementAt(i);
			
			if (q.attrs[i].toLowerCase().indexOf(ContextAttribute.eventNames[i].toLowerCase())== 0)
			{
				start = ContextAttribute.eventNames[2].length();
			
				start = q.attrs[i].indexOf("(",start) + 1;
				
				if (start != -1)
				{
					end = q.attrs[i].indexOf(")",start);
				
					if (end!=-1)
					{
						str = q.attrs[i].substring(start,end).trim();
						
						if (!ContextAttribute.checkName(str))
						{
							q.errStr = "Invalid context attribute: '" + q.attrs[i] + "' in the query string '" + query + "' \r\n";
							return q;
						}
					}
				}						
			}
			else {
			if (!ContextAttribute.checkName(q.attrs[i]))
			{
				q.errStr = "Invalid context attribute: '" + q.attrs[i] + "' in the query string '" + query + "' \r\n";
				return q;
			}
			
			if (q.type == QueryPlan.EVENT_SUBSCRIPTION)
			{
				for (j=0;j<ContextAttribute.eventNames.length;j++)
					if (q.attrs[i].toLowerCase().equals(ContextAttribute.eventNames[j].toLowerCase()))
						break;
				if (j==ContextAttribute.eventNames.length)
				{
					q.errStr = "Invalid event attribute: '" + q.attrs[i] + "' in an event subscription query '" + query + "' \r\n";
					return q;
				}					
			} }			
		}
		
		// parse the FROM clause: domain name
		if (!noFROM)
		{
		start = mark;
		/*end = query.indexOf(" where ",start);
		if (end == -1)
		{
			q.errStr = "Invalid query string: '" + query + "' \r\n";
			q.errStr += "WHERE clause is missing";
			return q;
		}*/
		end = query.indexOf(" where ", start);
		if (end == -1)
		{
			end = query.length();
			noWHERE = true;
		}		
		q.domain = query.substring(start,end).trim();
		
		if (!cds.checkDomain(q.domain))
		{
			q.errStr = "Invalid context domain: '" + q.domain + "' in the query string '" + query + "' \r\n";
			return q;
		}
		for (i=0;i<q.attrs.length;i++)
			if (!cds.checkName(q.domain,q.attrs[i]))
			{
				q.errStr = "Invalid context attribute: '" + q.attrs[i] + "' in the query string '" + query + "' \r\n";
				return q;
			}
		}
		else
			{
				q.domain = null;
				
				q.errStr = "Invalid query string: '" + query + "' \r\n";
				q.errStr += "FROM clause is missing";
				return q;	
			}
		
		// pare the WHERE clause: list of query conditions
		if (!noWHERE)
		{
		start = end + " where ".length();
		end = query.indexOf(" and ",start);
		v.removeAllElements();
		QueryCondition qc;
		StringTokenizer st;
		int start2, end2;
		while (end!=-1) {
			str = originQuery.substring(start, end).trim();
			st = new StringTokenizer(str);
			
			qc = new QueryCondition();
			qc.attr = st.nextToken();
			qc.op = st.nextToken();
			qc.constant = st.nextToken();
			String s;
			if (qc.constant.charAt(0)=='\"')
			while (true)
			{
				if (!st.hasMoreTokens())
					break;
				s = st.nextToken();				
				qc.constant += " " + s;
				if (s.charAt(s.length()-1)=='\"')
					break;
			}
			if (st.hasMoreTokens())
			{
				q.errStr = "Invalid predicate '" + str + "' in the query string '" + query + "' \r\n";
				return q;	
			}
			
			if (qc.constant.charAt(0)=='\"')
				if (qc.constant.charAt(qc.constant.length()-1)=='\"')
					qc.constant = qc.constant.substring(1,qc.constant.length()-1);
				else
				{
					start2 = str.indexOf("\"");
					start2++;
					end2 = str.indexOf("\"", start2);
					if (end2==-1)
					{
						q.errStr = "Invalid predicate '" + str + "' in the query string '" + query + "' \r\n";
						return q;	
					}
					else
						qc.constant = str.substring(start2,end2);
				}			
			if (!checkPredicate(q.domain, qc))
			{
				q.errStr = "Invalid predicate '" + str + "' in the query string '" + query + "' \r\n";
				return q;
			}
			else
				v.addElement((QueryCondition)qc);			
			
			start = end + " and ".length();
			end = query.indexOf(" and ",start);
		}
		end = query.length();
		str = originQuery.substring(start, end).trim();		
		st = new StringTokenizer(str);
			
		qc = new QueryCondition();
		qc.attr = st.nextToken();
		qc.op = st.nextToken();
		qc.constant = st.nextToken();
		String s;
		if (qc.constant.charAt(0)=='\"')
			while (true)
			{
				if (!st.hasMoreTokens())
					break;
				s = st.nextToken();				
				qc.constant += " " + s;
				if (s.charAt(s.length()-1)=='\"')
					break;
			}
		if (st.hasMoreTokens())
		{
			q.errStr = "Invalid predicate '" + str + "' in the query string '" + query + "' \r\n";
			return q;	
		}
		if (qc.constant.charAt(0)=='\"')
			if (qc.constant.charAt(qc.constant.length()-1)=='\"')
				qc.constant = qc.constant.substring(1,qc.constant.length()-1);
			else
			{
				start2 = str.indexOf("\"");
				start2++;
				end2 = str.indexOf("\"", start2);
				if (end2==-1)
				{
					q.errStr = "Invalid predicate '" + str + "' in the query string '" + query + "' \r\n";
					return q;	
				}
				else
					qc.constant = str.substring(start2,end2);
			}		
		if (!checkPredicate(q.domain, qc))
		{
			q.errStr = "Invalid predicate '" + str + "' in the query string '" + query + "'";
			return q;
		}
		else
			v.addElement((QueryCondition)qc);
			
		q.cons = new QueryCondition [v.size()];
		for (i=0;i<v.size();i++)
			q.cons[i] = (QueryCondition)v.elementAt(i);
		}
		else
			{
				q.cons = null;		
			}
		
		return q;
		
	} // pareseQuery	

	/**
	 * This function examines the satisfaction of a query predicate (condition).
	 * @param domain name of a context domain
	 * @param qc a query predicate
	 * @return TRUE if the condition is satisfied; otherwise FALSE
	 */
	public boolean checkPredicate(String domain, QueryCondition qc) {
		
		if (!cds.checkName(domain, qc.attr))
			return false;	
		
		if (qc.op.equals(">") || qc.op.equals("<") || qc.op.equals(">=") || qc.op.equals("<=") || qc.op.equals("==") || qc.op.equals("=") || qc.op.equals("!=") || qc.op.equals("<>"))
			{}
		else
			return false;				
		
		if (!ContextAttribute.checkValue(qc.attr,qc.constant))
			return false;
		
		return true;
		
	}  // checkPredicate
	
	//debugging purpose
	/*public static void main (String argvs[]) {	
		
		String domains [] = {"person", "shop", "office","health"};
		String attrs [][] = {{"name","location", "getContextChange"}, 
							 {"name", "egg_loc", "egg_price", "hasAdvertisement"}, 
							 {"name", "room_status","isVacant"},
							 {"name","hasECGData"},};
		int EventServerSocketPort = 9999;
		
		ServiceManager sm = new ServiceManager(domains, attrs, EventServerSocketPort);
		
		sm.cds.psg.PSFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);		
		
		sm.cds.assign("office","name","Keith's Office");
		sm.cds.assign("person","name","Keith");
		//sm.cds.assign("shop","location","VivoCity");
		sm.cds.assign("shop","hasAdvertisement","true");
		sm.cds.assign("office","room_status","meeting_in_progress");
		sm.cds.assign("office","isVacant","true");
		sm.cds.assign("person","location","S14 #06-20, SOC, NUS, 3 Science Drive 2, Singapore, 117543");
		sm.cds.assign("shop","name","Fair Price (NTUC)");
		sm.cds.assign("shop","egg_loc","2nd Floor");
		sm.cds.assign("shop","egg_price","5");
		sm.cds.assign("health", "name", "NUH");
		
		QueryProcessor qp = new QueryProcessor(sm.cds);
		String query;
		QueryPlan q;
		
		/*query = "SELECT name, location from PERSON where name = \"Keith\"";		
		q = qp.parseQuery(query);
		q.print();		
		System.out.println();*/
		
		/*query = "SUBSCRIBE isVacant FROM OFFICE WHERE name = \"Keith's Office\"";
		q = qp.parseQuery(query);
		q.print();
		System.out.println();*/
		
		/*query = "SUBSCRIBE isVacant FROM OFFICE WHERE room_status = \"meeting_in_progress\"";
		q = qp.parseQuery(query);
		q.print();
		System.out.println();*/
		
		/*query = "SUBSCRIBE hasAdvertisement FROM SHOP WHERE name = \"Fair Price (NTUC)\"";
		q = qp.parseQuery(query);
		q.print();
		System.out.println();*/
		
		/*query = "SELECT egg_loc, egg_price FROM SHOP WHERE name = \"Fair Price (NTUC)\"";
		q = qp.parseQuery(query);
		q.print();
		System.out.println();*/
		
		/*query = "SUBSCRIBE getContextChange(location) FROM PERSON WHERE name = \"Keith\"";
		q = qp.parseQuery(query);
		q.print();
		System.out.println();*/
		
		/*query = "SUBSCRIBE hasECGData FROM HEALTH WHERE name = \"NUH\"";
		q = qp.parseQuery(query);
		q.print();
		System.out.println();*/	
		/*
		System.exit(0);
		
	}	// main*/

}  // class